package com.huakai.springenv.aspect.profiler;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.http.HttpServletRequest;
import java.util.*;

/**
 * 用于记录代码耗时，当代码耗时超过指定阈值时打印告警日志
 */
public class DBTimeProfile {

    private static final Logger log = LoggerFactory.getLogger(DBTimeProfile.class);
    private static final ThreadLocal<LinkedList<TimeProfileElement>> logs = new ThreadLocal<>();
    private static final ThreadLocal<TimeProfileElement> currentElement = new ThreadLocal<>();
    private static final ThreadLocal<HttpServletRequest> request = new ThreadLocal<>();
    private static final ThreadLocal<Integer> currentThreshold = new ThreadLocal<>();
    private static volatile int threshold = 100;// 超时时间，单位毫秒
    private static final String timeout = "DBTimeProfile timeout ";

    private DBTimeProfile() {
    }

    private static void enter() {
        enter("");
    }

    /**
     * 在需要监控耗时的方法体前调用
     *
     * @param tag 方法体的名字
     */
    public static void enter(String tag) {
        if (logs.get() == null) {
            return;
        }
        TimeProfileElement cur = currentElement.get();

        TimeProfileElement t = new TimeProfileElement();
        t.logEnterTime();
        t.setTag(tag);
        t.parent = cur;
        t.setDeep(cur == null ? 0 : cur.getDeep() + 1);
        currentElement.set(t);

        logs.get().add(t);
    }

    /**
     * 在需要监控耗时的方法体后调用
     */
    public static void release() {
        LinkedList<TimeProfileElement> s1 = logs.get();
        TimeProfileElement cur = currentElement.get();
        if (s1 == null || cur == null) {
            return;
        }
        if (s1.size() <= 1) {
            // throw new IllegalStateException("release failed,will stop this timeProfile, enter/release 必须成对出现");
            log.warn("release failed,will stop this timeProfile, enter/release 必须成对出现", new IllegalStateException());
            clear();
            return;
        }
        cur.logReleaseTime();
        currentElement.set(cur.parent);
    }

    /**
     * 在Filter等入口处调用,开始计时，当接入了spring-boot-ext项目时，你不需要调用此方法，框架会调用。
     */
    public static void start() {
        logs.set(new LinkedList<>());
        enter();
    }
    /**
     * 在Filter等入口处调用,开始计时，当接入了spring-boot-ext项目时，你不需要调用此方法，框架会调用。
     */
    public static void start(HttpServletRequest httpServletRequest) {
        logs.set(new LinkedList<>());
        request.set(httpServletRequest);
        enter();

    }

    /**
     * 在Filter等入口处调用,结束当前计时，如果超过日志，会打印告警日志，当接入了spring-boot-ext项目时，你不需要调用此方法，框架会调用。
     */
    public static void end() {
        end("undefined");
    }

    /**
     * 在Filter等入口处调用,结束当前计时，如果超过日志，会打印告警日志，当接入了spring-boot-ext项目时，你不需要调用此方法，框架会调用。
     */
    public static void end(String methodName) {
        TimeProfileElement cur = currentElement.get();
        if (cur == null) {
            return;
        }
        if (cur.parent != null) {
            log.warn("timeProfile methodName:{} failed, enter/release 必须成对出现", methodName);
            clear();
            return;
        }
        cur.logReleaseTime();

        long timeConsume = cur.getCostTimeMillis();
        Integer threshold = currentThreshold.get();
        if (threshold == null) {
            threshold = DBTimeProfile.threshold;
        }
        if (cur.getCostTimeMillis() > threshold) {
            // 输出日志
            StringBuilder sb = new StringBuilder();
            HttpServletRequest httpRequest = request.get();
            if (httpRequest != null) {
                sb.append(timeout).append(timeConsume).append("ms >").append(threshold).append("ms, url=").append(httpRequest.getRequestURI());
            } else {
                sb.append(timeout).append(timeConsume).append("ms > methodName:").append(methodName);
            }
            List<TimeProfileElement> list = new ArrayList<>(logs.get());

            for (TimeProfileElement s : list) {
                sb.append("\r\n\t");
                for (int i = 0; i < s.getDeep(); i++) {
                    sb.append("-");
                }
                Long consume = s.getCostTimeMillis();
                sb.append(consume * 100 / timeConsume).append("%");
                sb.append("  ").append(consume).append("ms");
                if (s.getTag() != null) {
                    sb.append("  ").append(s.getTag());
                }
            }
            log.warn("{}", sb);
        }
        clear();
    }

    private static void clear() {
        request.remove();
        currentElement.remove();
        logs.remove();
        currentThreshold.remove();
    }

    /**
     * 设置报警阈值，这是全局配置，只要在应用启动时设置一次即可,单位：ms
     * <br/>
     *
     * @param threshold 告警阈值
     */
    public static void setThreshold(int threshold) {
        DBTimeProfile.threshold = threshold;
    }

    /**
     * 设置当前线程调用栈的报警阈值，这个配置只会在当前线程、本次监控生效。
     *
     * @param threshold 告警阈值
     */
    public static void setCurrentThreshold(int threshold) {
        currentThreshold.set(threshold);
    }

    /**
     * 获取当前线程的报警报警阈值
     *
     * @return 告警阈值
     */
    public static Integer getCurrentThreshold() {
        return currentThreshold.get();
    }

    /**
     * dump堆栈信息
     */
    private static String dump() {
        if (logs.get() == null) {
            return null;
        }
        TimeProfileElement cur = currentElement.get();
        cur.logReleaseTime();

        Long timeconsume = 0L;
        for (TimeProfileElement s : logs.get()) {
            if (s.deep == 0) {
                timeconsume += s.getCostTimeMillis();
            }
        }
        if (timeconsume < (long) 0) {
            return null;
        }
        // 输出日志
        StringBuilder sb = new StringBuilder();
        sb.append(timeout).append(timeconsume).append("ms ");
        List<TimeProfileElement> list = new ArrayList<>(logs.get());
        Collections.reverse(list);
        for (TimeProfileElement s : list) {
            Long consume = s.getCostTimeMillis();
            if (consume == 0) {
                continue;
            }
            sb.append("\r\n\t");
            for (int i = 0; i < s.getDeep(); i++) {
                sb.append("  ");
            }
            sb.append("-");
            sb.append(consume * 100 / timeconsume).append("%");
            sb.append("  ").append(consume).append("ms");
            if (s.getTag() != null) {
                sb.append("  ").append(s.getTag());
            }
        }
        clear();

        return sb.toString();
    }

    public static void main(String[] args) throws Exception {
        // 20次只会打印10次
        for (int j = 0; j < 2; j++) {
            DBTimeProfile.setCurrentThreshold(0);
            DBTimeProfile.start();
            for (int i = 0; i < 5; i++) {
                try {
                    try {
                        DBTimeProfile.enter("in" + i);
                        Thread.sleep(1);
                        try {
                            DBTimeProfile.enter("in1" + i);
                            Thread.sleep(1);
                        } finally {
                            DBTimeProfile.release();
                        }
                        try {
                            DBTimeProfile.enter("in2" + i);
                            Thread.sleep(1);
                        } finally {
                            DBTimeProfile.release();
                        }
                    } finally {
                        DBTimeProfile.release();
                    }
                } finally {
                }
            }
            DBTimeProfile.end("fuck!");
        }
    }

    private static class TimeProfileElement {

        // 基线时间，为了减小timeMillis的占用空间
        private static final long BASE_TIME = System.currentTimeMillis();

        private int timeMillis;
        private int deep;
        private String tag;

        private TimeProfileElement parent;

        void logEnterTime() {
            timeMillis = (int) (System.currentTimeMillis() - BASE_TIME);
        }

        void logReleaseTime() {
            timeMillis = (int) (System.currentTimeMillis() - BASE_TIME - timeMillis);
        }

        long getCostTimeMillis() {
            return timeMillis;
        }

        int getDeep() {
            return deep;
        }

        void setDeep(int deep) {
            this.deep = deep;
        }

        String getTag() {
            return tag;
        }

        void setTag(String tag) {
            this.tag = tag;
        }

    }
}
